Skip to content

FE-1602: Restyle the Petrinaut architecture docs - #9527

Merged
kube merged 28 commits into
mainfrom
claude/petrinaut-arch-docs-styling-554323
Sep 5, 2026
Merged

FE-1602: Restyle the Petrinaut architecture docs#9527
kube merged 28 commits into
mainfrom
claude/petrinaut-arch-docs-styling-554323

Conversation

@kube

@kube kube commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Important

Appearance, plus the diagram renderer. No change to the architecture model or to page content, beyond two overlapping edge labels shortened on one diagram.

Summary

A new look for the architecture docs: Swiss, monochrome, and much quieter. One neutral ramp replaces Starlight's blues and greys, the chrome drops its dividers and steps back until it is pointed at, and everything that moves — the sidebar collapse, the resize, the bands under the header — now moves on one curve. Diagrams follow the page theme instead of staying white in the dark.

Before

9527-before.mp4

After

9527-after.mp4

Bands, script against scroll timeline

9527-fade-paths.mp4

Links

Changes

Appearance

  • One neutral ramp across both themes
  • No rules drawn in the chrome except the sidebar's depth guide, which carries information
  • Controls read by fill rather than outline: the search field, the diff-build compare chips
  • Accent tokens point at the strongest foreground, taking the blue out of prose links, the active nav row and the active table-of-contents row
  • Body text in Inter, 17px on a 1.65 leading, with a shorter heading scale
  • Blocks 1.5rem apart, 2.25em above a section heading
  • Header drops its social links and the rule that divided them
  • Theme select's focus ring restated in the palette
  • Sidebar and table-of-contents rows are pills; code frames rounded and flat
  • Rails rest at 70% and the collapse toggle at 50%, reaching full strength over 300ms, off under prefers-reduced-transparency
  • A blur-and-tint band under the header and a mirrored one at the foot of the nav, both grown from scroll, so an unscrolled page has nothing laid over it
  • Bands driven by a CSS scroll timeline, so they are right on the first frame; the script stays as the fallback for Firefox, which has shipped neither half
  • CSS minified with esbuild, because Lightning CSS folded the timeline into the animation shorthand, where no browser accepts one, and the bands were dead in built output only
  • Left rail rests at 52.5% and the collapse button at 37.5%, and the button sits 16px from the left edge, matching its distance from the top

Sidebar motion

  • Collapse animates the pane, the content column, the handle and both panels on one curve
  • Content inset derives from the pane width, replacing a jump to an unrelated 2rem
  • Pane hides with visibility, so its rows travel with it and still leave the tab order at rest
  • All of it behind prefers-reduced-motion

Resize handle

  • A 2px mark fades in on hover inside the unchanged 6px hit area, and stays lit for the whole drag
  • A gesture marks itself and the transitions stop applying, so the pane tracks the pointer rather than trailing it
  • Width applies once per frame and persists once per gesture, for a held arrow key as well as a drag

Diagrams

  • Recoloured through custom properties, so they follow the page's light and dark themes
  • Set in Inter, and drawn with 16px of padding rather than D2's default 100
  • Inlined into the page at build time, which is what lets page CSS and the tooltips reach them
  • Two overlapping edge labels shortened on the GPU batch lifecycle diagram

Test coverage

  • withThemeableColours unit tests

    Scoping to D2's own class, the drawn colour as every fallback, markup without a scope left alone, and the mask channels left unthemed — theming fill="white" inverts D2's label knockout and erases every connection.

  • Contrast measured on the page, both themes

    The chrome rests behind an opacity, so what a reader sees is composited: worst row of 107 sidebar links, 6 table-of-contents rows, the heading, the toggle and the theme select.

  • lint:tsc and the production build
  • Scripted DOM probes

    One toggle and one drag; the keyboard gesture's single storage write; diagram theming and badges across both themes.

  • Bands measured against the scroll, on both paths

    Head and foot, page and rail, at rest, mid-ramp and past it, and a rail with nothing to scroll showing neither.

  • Bands measured against first contentful paint

    The script's write lands 4ms after FCP idle and 181ms after it under a 6x CPU throttle; the CSS path writes nothing, so it cannot be late.

  • Built CSS asserted after every build

    scripts/check-built-css.mjs fails on a timeline folded into the animation shorthand, on a missing animation-timeline, and on a missing @property registration.

  • Composited contrast measured on the page, both themes

    At the chosen resting opacities rail text is 3.1:1 and the button's icon 2.5:1 dark / 2.3:1 light, below the 4.5:1 and 3:1 WCAG asks; recorded in the stylesheet next to the tokens.

How to test

  • Open Petrinaut arch docs preview on Vercel
  • Toggle the sidebar from the control left of the logo
  • Expect pane, content column and on-this-page panel to move together, no jump
  • Hover the pane's trailing edge, then drag it
  • Expect a thin mark to fade in, the width to follow the pointer with no lag, and the mark to stay lit outside the strip
  • Focus that edge, then hold ArrowLeft and ArrowRight
  • Scroll the page, and scroll the nav
  • Expect a band to grow under the header and at the foot of the nav, and nothing over an unscrolled page
  • Scroll down, then reload
  • Expect the bands to be there in the first painted frame, with no flash (Chrome, Edge, Safari; Firefox still fades them in a frame late)
  • Header > theme select > Light
  • Expect the same monochrome palette inverted, diagrams included

@kube kube self-assigned this Sep 3, 2026
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
petrinaut-docs Ready Ready Preview Sep 5, 2026 4:16pm UTC
3 Skipped Deployments
Project Deployment Actions Updated
hash Ignored Ignored Preview Sep 5, 2026 4:16pm UTC
hashdotdesign-tokens Ignored Ignored Preview Sep 5, 2026 4:16pm UTC
petrinaut Skipped Skipped Sep 5, 2026 4:16pm UTC

Request Review

kube added 2 commits September 4, 2026 00:27
Restate Starlight's palette and type scale as one flat monochrome
system, so the chrome reads as a single surface divided by hairlines
and colour is left to the things that carry meaning.

Collapsing the left panel now animates: the pane hides with
visibility rather than display, the content inset derives from the
pane width, and every box sized from it transitions on one curve.
A drag or a held arrow key already tracks its input, so the handle
marks the gesture and the transitions stand aside.
The borders framing the header, the sidebar and the on-this-page
column are gone, leaving the sidebar's depth guide as the only rule
drawn in the chrome, where it says how deep a row sits in the tree.

The search field and the compare chips now read by fill rather than
by an outline, one step off the page background in either theme.
Both rails and the collapse toggle sit at 70% until the pointer or
keyboard focus reaches them, over 300ms, with the whole rail as the
hover target. prefers-reduced-transparency turns the effect off.

A 30px band under the header blurs and tints the content passing
beneath it. backdrop-filter cannot ramp on its own, so the ramp is
three stacked layers of increasing radius, each masked to a shorter
band, and each filtering what the one beneath it produced.

Blocks now sit 1.5rem apart with 2.25em above a section heading, and
body copy is 17px: Starlight defines --sl-text-body but never applies
it, so the size had to be set here.
The band ran both to 30px, which hid the blur: its strongest layers
sit at the top, which is exactly where the tint is opaque. The tint
keeps its 30px and the blur now runs to 60px, so below the tint there
is a stretch where content is blurred and still visible.

Radii go up with it, 4px to 16px.
The three layers now differ by height rather than by mask alone. All
are anchored under the header and run to a different depth: 60px at
1px of blur, 40px at 2px, 20px at 4px. They overlap towards the
header and thin out to one at the bottom, so the tallest and weakest
is the layer that meets unblurred content.

Each fades out over its own lower half, so a layer ending reads as a
soft edge rather than a line across the page.
The band's construction moves to a shared class so the rail and the
content column build it from one set of rules.

The rail scrolls independently of the page, so its band keys off the
pane's own scrollTop and appears only once rows have gone up behind
the header. Shown unconditionally it would sit on the first row of an
unscrolled rail and dim it for nothing.
@vercel
vercel Bot temporarily deployed to Preview – petrinaut September 4, 2026 00:08 Inactive
Height and opacity both come off `--pnd-nav-progress`, which the
header script writes from the rail's scrollTop, 0 to 1 over the first
60px. At rest the band has no height, so it stays off the first row of
an unscrolled rail. Nothing eases it: the value already tracks the
scroll frame by frame, and a transition would leave the band lagging
the rows it covers.

The ramp is now two layers rather than three, 60px at 1px of blur and
30px at 2px, in both places the band is used.
@cursor

cursor Bot commented Sep 5, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Docs-site presentation and diagram rendering only; no auth, data, or API changes. Main risk is subtle CSS/build regressions (fade bands, minification), which the post-build CSS check targets.

Overview
Restyles the Petrinaut architecture docs site around a flat monochrome Starlight theme: shared neutral ramp, Inter body text, quieter chrome (no header/sidebar dividers, pill nav, fill-based controls), and rails/toggles that rest dimmed until hover or focus (prefers-reduced-transparency lifts them). Adds scroll-driven blur/tint bands under the header and at the nav foot, with CSS animation-timeline where supported and a SiteTitle.astro scroll fallback for Firefox; sidebar collapse/resize now animates on one curve and resize uses data-pnd-resizing plus rAF-throttled width updates.

Diagram pipeline: @local/petrinaut-arch-docs renders D2 with vendored Inter, lower padding, and post-processes SVGs via withThemeableColours so hosts can override --pnd-diagram-* tokens; the docs app inlines diagram <img>s at build time (rehypeInlineDiagrams) and maps dark-mode colours in chrome.css. Build hardening: Vite uses cssMinify: "esbuild" to avoid Lightning CSS breaking scroll timelines, and scripts/check-built-css.mjs runs after astro build. Header GitHub social link removed; GPU lifecycle diagram edge labels shortened.

Reviewed by Cursor Bugbot for commit e582f87. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved moderate issues affect accessibility, persistence behavior, content scope, and SVG processing coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Restyles the Petrinaut architecture docs, animates sidebar behavior, adds Inter fonts, and enables theme-aware inline diagrams.

Changes:

  • Introduces monochrome styling, updated typography, fades, and responsive animations.
  • Improves sidebar resizing, persistence, and scroll effects.
  • Adds themeable SVG diagrams and supporting documentation.
File summaries
File Description
yarn.lock Resolves new Markdown and HAST dependencies.
libs/@local/petrinaut-arch-docs/src/emit/d2.ts Adds themeable SVG processing. Moderate: Add focused tests for scoped-rule insertion and unchanged output without a D2 scope.
libs/@local/petrinaut-arch-docs/src/cli.ts Supplies the font path during D2 rendering.
libs/@local/petrinaut-arch-docs/README.md Documents diagram theming. Nit: Use an opaque host-background value for n7 to avoid lines crossing labels.
libs/@local/petrinaut-arch-docs/fonts/OFL.txt Adds the Inter font license.
libs/@local/petrinaut-arch-docs/content/diagrams/gpu-batch-lifecycle.d2 Moderate: Revert the semantic label edits or document and validate them as content changes.
apps/petrinaut-docs/src/styles/chrome.css Implements the redesign and animations. Moderate: Raise idle opacity values to meet control and text contrast requirements.
apps/petrinaut-docs/src/plugins/inline-diagrams.mjs Inlines generated SVG diagrams.
apps/petrinaut-docs/src/components/SiteTitle.astro Adds fades and revised resize behavior. Moderate: Persist keyboard-resized width once on keyup or blur, not on every repeated keydown.
apps/petrinaut-docs/src/components/Head.astro Emits and preloads Inter.
apps/petrinaut-docs/README.md Documents styling, motion, diagrams, and fonts.
apps/petrinaut-docs/package.json Adds Markdown-processing dependencies.
apps/petrinaut-docs/astro.config.mjs Registers diagram inlining and Inter.
Review details
  • Files reviewed: 12/15 changed files
  • Comments generated: 5
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/petrinaut-docs/src/components/SiteTitle.astro Outdated
Comment thread apps/petrinaut-docs/src/styles/chrome.css Outdated
Comment thread libs/@local/petrinaut-arch-docs/src/emit/d2.ts Outdated
Comment thread libs/@local/petrinaut-arch-docs/README.md Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread apps/petrinaut-docs/src/plugins/inline-diagrams.mjs
kube added 3 commits September 5, 2026 04:29
A held arrow key repeats about thirty times a second and each repeat wrote
to localStorage, which writes synchronously. The width a keyboard gesture
reaches is now kept in hand and written when the gesture ends on keyup or
blur, the same as a drag already did.
The rails and the header controls rest behind an opacity, so what a reader
sees is their colour composited against the page, not the colour written
in the stylesheet. Starlight's gray-3 arrives at 3.6:1 that way where body
text owes 4.5:1, and at 50% the toggle's icon is 2.3:1 against the 3:1 a
control owes.

The fade is kept exactly as designed and the colour underneath it is
raised instead, to the greys whose composite clears each threshold. The
theme select moves to the rails' opacity: it is the one header control
carrying a visible label, and at 50% over a white page no colour reaches
4.5:1, pure black included.

Measured on the page rather than computed from the tokens: worst of 107
sidebar rows and 6 table-of-contents rows, both themes, 4.93:1 dark and
5.09:1 light for text, 3.68:1 and 3.19:1 for the icon.
The theming hook matches a format d2 owns: the scope class it puts on a
drawing, and the colours it wrote. A change to either would stop every
host override silently, with the diagrams still rendering in the palette
they were drawn in. Tests cover the scoping, the drawn colour standing as
each fallback, markup without a scope passing through, and the mask
channels being left alone, which is the failure that erased every
connection line the first time.

The README offered n7 as transparent. It is the drawing's paper and also
fills the rects that knock connections out from behind their labels, so a
transparent one leaves every label struck through.
@kube
kube requested review from CiaranMn and YannisZa September 5, 2026 02:29
@kube kube changed the title Restyle the architecture docs site and animate the sidebar FE-1602: Restyle the Petrinaut architecture docs Sep 5, 2026
The bands took their progress from a number the header script wrote on
every scroll, so a page restored to a scroll position painted once
without them and then with them. Measured against first contentful
paint, that write lands 4ms late on an idle machine and 181ms late under
a 6x CPU throttle, which is the flash: about eleven frames of a page
whose bands arrive after the content.

A scroll-driven animation resolves the same progress from layout, on the
first frame, and takes the scroll listeners with it. The rail's bands
reach its scroller through a named timeline, since they are drawn in the
header rather than inside the rail, and the foot band runs reversed over
the last stretch of the scroll, which is how what is left below the fold
falls out of a timeline that only counts distance travelled. Its
ResizeObserver goes too: an opened group changes the timeline's own
extent.

Firefox has shipped neither half, so the script stays as the fallback
there, gated on the same condition the stylesheet tests. The ramp is a
CSS token now, read by the script rather than repeated in it.
kube added 2 commits September 5, 2026 17:27
…'s inset

The rail now rests at 52.5% and the collapse button at 37.5%, three
quarters of what each sat at. The right-hand panel and the theme select
are left where they were.

This costs contrast, and no colour buys it back: composited, rail text
lands at 3.1:1 against the 4.5:1 body text owes, and the button's icon
at 2.5:1 dark and 2.3:1 light against 3:1. At 52.5% over a white page
even pure black reaches only 4.35:1, and at 37.5% only 2.6:1, so the
lever is the resting opacity rather than the grey. Both still come to
full strength on hover and on keyboard focus, and reduced-transparency
turns the fade off. The note in the stylesheet records the measured
figures rather than the ones it used to claim.

The button also moves 4px left, to sit 16px from the viewport's left
edge, which is what the 64px header leaves above a 32px button. The
title wrapper clips its overflow, so it is extended by the same 4px and
given it back as padding: the clip boundary travels with the button and
the logo beside it does not move.
Lightning CSS rewrote the bands' rules as `animation: linear both
pnd-fade-grow scroll(root)`. A timeline is not accepted in the
`animation` shorthand — it was taken out while the spec settled — so the
declaration was invalid and `animation-name` computed to `none`.

Every built page therefore had no fade bands at all, and nothing said
so: the dev server does not minify, so it kept working, and the header
script had already stood down because the browser does support the CSS
path. Only the published site was affected.

The docs app minifies CSS with esbuild instead, which leaves the rules
alone. `scripts/check-built-css.mjs` then reads the built stylesheets
and fails the build on the shape that caused this, on a missing
`animation-timeline`, and on a missing `@property` registration, so a
build that draws no bands cannot be published quietly. Checked against
the production preview: the band is 60px with the animation attached,
where before the fix it was 0px.
@vercel
vercel Bot temporarily deployed to Preview – petrinaut September 5, 2026 15:57 Inactive
@kube
kube requested a review from vilkinsons September 5, 2026 16:00

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 077f05c. Configure here.

Comment thread apps/petrinaut-docs/src/styles/chrome.css
Splitting `--pnd-rail-idle-opacity` out so the rail could rest lower than
the right-hand panel left it out of the `prefers-reduced-transparency`
rule, which still lifted only the two tokens that existed before. Every
other piece of chrome went to full strength and the rail stayed at
52.5%, so the preference missed the one element that recedes furthest,
and at that opacity its text is the 3.1:1 the stylesheet already records.

Nothing tied a new token to that rule, so the built-CSS check now ties
them: it collects every `--pnd-*-idle-opacity` declared and fails on any
the rule does not lift. Run against the branch before the fix it named
`--pnd-rail-idle-opacity` on its own.

Measured with the media feature emulated: rail, right panel, toggle and
theme select all resolve to 1 with the preference set, and to 0.525,
0.7, 0.375 and 0.7 without it.
@kube
kube added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 757cae9 Sep 5, 2026
55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

3 participants